﻿/* Engine.cs

	Infinite Rogue
	By Dan Alexander, Andy Law
	July, 2012

	Cross platform roguelike using C#, Mono, WinForms and GDI.
	
	LICENSE: New BSD License
*/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing;
using InfiniteRogue.Rules;
using InfiniteRogue.AI;
using InfiniteRogue;

    /// <summary>
    /// Contains the map, the player, and oh so much more. Accepts commands to control the player. Fires an event when the map changes.
    /// </summary>
    public class Engine : System.Windows.Threading.DispatcherObject
    {
        Map _map;
        private Character _player;
        private List<Character> _players;

        public Engine()
        {
            Rules = new EngineRules(this);

            _players = new List<Character>();
            _player = new Character();
            _player.Name = "Player";
            _player.Stats.StatChanged += new StatSystem.StatChangedEventHandler((string stat, int oldVal, ref int newval) =>
            {
                Log.Instance.SystemLog(string.Format("{3}'s {0} changed from {1} to {2}", stat, oldVal, newval, _player.Name));
            });

            _player.Stats.StatChanged += new StatSystem.StatChangedEventHandler((string stat, int oldVal, ref int newval) =>
            {
                if (stat == StatSystem.EXP)
                {
                    while (Rules.Bounty.LevelFromExp(_player[StatSystem.EXP]) > _player[StatSystem.LVL])
                    {
                        Rules.Bounty.LevelUp(_player);
                        Log.Instance.GameLog(string.Format("{0} got a level up!!!", _player.Name), Log.LogLevel.HighImportance);
                    }
                }
            });

            Rules = new EngineRules(this);

            CurrentLevel = 0;
            GenerateLevel();

            _player.SetCoords(_map.PlaceCharacterOnMap());

            _map.UpdateDiscoveredTiles(_player);

            if(MapChangedEvent != null)
                MapChangedEvent(this);
        }

        public Map Map
        {
            get { return _map; }
            private set { _map = value; }
        }

        public Character Player
        {
            get
            {
                return _player;
            }
            private set
            {
                _player = value;
            }
        }

        public int CurrentLevel { get; private set; }

        public EngineRules Rules { get; private set; }

        public delegate void MapChangedEventHandler(Engine engine);

        /// <summary>
        /// This should be fired any time a redraw of the game is suggested
        /// </summary>
        public event MapChangedEventHandler MapChangedEvent;

        /// <summary>
        /// Gets the object in the world at the position, or the map tile
        /// </summary>
        /// <param name="x"></param>
        /// <param name="y"></param>
        /// <returns></returns>
        public TileData TileDataAt(int x, int y)
        {
            if (_player.X == x && _player.Y == y)
            {
                _player.Tile.Color = _player.HealthColor;
                return _player.Tile;
            }

            TileData mapTile = Map.GetTile(x, y);

            Character npc = _players.FirstOrDefault(c => c.Pos.Level == CurrentLevel && c.X == x && c.Y == y);
            if (npc != null && mapTile.Discovered)
            {
                return npc.Tile;
            }

            return mapTile;
        }

        public TileData TileDataAt(MapCoord coord) { return TileDataAt(coord.x, coord.y); }

        /// <summary>
        /// If the map position is not blocked, the player moves and discovers her area
        /// </summary>
        /// <param name="deltaX"></param>
        /// <param name="deltaY"></param>
        public void MovePlayer(int deltaX, int deltaY)
        {
            int goalX = _player.X + deltaX;
            int goalY = _player.Y + deltaY;

            if (goalX < 0 || goalX >= Map.Width || goalY < 0 || goalY >= Map.Height)
            {
                return; //we can't move outside the bounds of the map
            }

            // Can't step on people
            Character blockingCharacter = _players.FirstOrDefault(p => p.Level == CurrentLevel && p.X == goalX && p.Y == goalY);
            if (blockingCharacter != null)
            {
                if (StunMode)
                {
                    Rules.Combat.StunAttack(_player, blockingCharacter);
                }
                else if (CritMode)
                {
                    Rules.Combat.CriticalAttack(_player, blockingCharacter);
                }
                else
                {
                    Rules.Combat.MeleeAttackSingle(_player, blockingCharacter);
                }
                WorldTurn();
                return;
            }

            //open door if you can
            Map.OpenDoor(goalX, goalY);
            if(!Map.GetTile(goalX, goalY).Passable)
            {
                //we can't move onto an impassable tile
                return;
            }

            _player.X = goalX;
            _player.Y = goalY;
            WorldTurn();
            Map.UpdateDiscoveredTiles(_player);
        }

        public bool IsSpotPassible(MapCoord spot)
        {
            return Map.GetTile(spot.x, spot.y).Passable && !_players.Any(p => p.Level == spot.Level && p.X == spot.x && p.Y == spot.y);
        }

        /// <summary>
        /// Asks the map if there are stairs down. If so, a new level is generated and the player goes down.
        /// </summary>
        public void StairDown()
        {
            if (Map.StairDown(Player.X, Player.Y))
            {
                _player.SetCoords(GenerateLevel());
                _map.UpdateDiscoveredTiles(_player);
                WorldTurn();
            }
        }

        public void PlayerMelee()
        {
            Rules.Combat.MeleeAttackRadial(_player, 1);
            WorldTurn();
        }

        public void AllocatePlayerFreeStat(string stat)
        {
            if (_player[StatSystem.STATPOINTS] > 0 && StatSystem.IsStatpointStat(stat))
            {
                _player[StatSystem.STATPOINTS] -= 5;
                _player[stat] += 5;
            }
        }

        public void AllocatePlayerFreeSkill(string skill)
        {
            if (_player[StatSystem.SKILLPOINTS] > 0 && StatSystem.IsSkillStat(skill))
            {
                _player[StatSystem.SKILLPOINTS] -= 5;
                _player[skill] += 5;
            }
        }

        /// <summary>
        /// Creates a new level and returns the starting coordinates
        /// </summary>
        /// <returns>If level 0, start position, else up stairs position</returns>
        public MapCoord GenerateLevel()
        {
            //Create Map
            CurrentLevel = Math.Max(1, CurrentLevel * 2);
            Generator gen = new Generator(100, 100);
            gen.Build();
            _map = gen.Map();
            _map.LevelNumber = CurrentLevel;
            int[] stairs = gen.GetStairs();
            _map.AddStairs(stairs);
            foreach (var room in gen.GetRooms())
            {
                int area = room.roomdim.rheight * room.roomdim.rwidth;
                int monsterCount = area / 20;
                MapCoord spawnPoint = new MapCoord()
                {
                    Level = CurrentLevel,
                    x = room.tl.x + room.roomdim.rwidth / 2,
                    y = room.tl.y + room.roomdim.rheight / 2
                };

                for (int i = 0; i < monsterCount; ++i)
                {
                    SpawnRandom(spawnPoint);
                }
            }

            return new MapCoord() { x = stairs[0], y = stairs[1], Level = CurrentLevel };
        }

        public bool MoveNPC(Character c, int deltaX, int deltaY)
        {
            int goalX = c.X + deltaX;
            int goalY = c.Y + deltaY;

            if (goalX < 0 || goalX >= Map.Width || goalY < 0 || goalY >= Map.Height)
            {
                return false; //we can't move outside the bounds of the map
            }

            // Can't step on others (including yourself)
            if (_players.Any(p => p.X == goalX && p.Y == goalY)) return false;
            if (_player.X == goalX && _player.Y == goalY && _player.Level == CurrentLevel) return false;

            // NPC can't open door if next line is disabled
            // Map.OpenDoor(goalX, goalY);

            //we can't move onto an impassable tile
            if(!Map.GetTile(goalX, goalY).Passable)
            {                
                return false;
            }

            c.X = goalX;
            c.Y = goalY;
            return true;
        }

        private void SpawnRandom(MapCoord place)
        {
            Character mob = InfiniteRogue.GameContent.RandomMonster.Instance.GetRandomMonster(CurrentLevel);
            mob.LastEnemy = _player;
            SpawnCharacter(mob, place);
        }

        private void SpawnCharacter(Character mob, MapCoord place)
        {
            mob.SetCoords(place);

            mob.Stats.StatChanged += new StatSystem.StatChangedEventHandler((string stat, int oldval, ref int newval) =>
            {
                if (stat == StatSystem.HP && newval <= 0)
                {
                    newval = 0;
                    KillMonster(mob);
                }
            });

            mob.AI = (AI)new SmartMonsterAI(mob, this);

            _players.Add(mob);
        }

        private void KillMonster(Character c)
        {
            Rules.Bounty.ProcessCombatVictory(_player, c, this);
            _players.Remove(c);
        }

        private void WorldTurn()
        {
            foreach (var mob in _players)
            {
                if (mob.Pos.Level == CurrentLevel && mob.AI != null) mob.AI.Turn();
                mob.Stats.DecrementStatuses();
            }
            _player.Stats.DecrementStatuses();
            MapChangedEvent(this);
        }

        public void RestPlayer()
        {
            _player.Stats[StatSystem.HP] += 1;
            WorldTurn();
        }

        public void DefendPlayer()
        {
            _player[StatSystem.ALERT] = 1;
            WorldTurn();
        }

        public bool StunMode { get; set; }
        public bool CritMode { get; set; }

        public List<Character> TargetArea(MapCoord target, int radius)
        {
            return new List<Character>(from p in _players where p.Pos.InTarget(target, radius) select p);
        }
    }

